iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0

大綱

  • Simple Using
  • Anatomy
  • Key properties
  • Modal navigation drawer
  • Bottom navigation drawer
  • Style

Simple Using

一樣先簡單帶大家做一下基本的用法與架構

navigationView

建立 drawer 最主要的元件,當中有許多屬性可以調整,稍後會陸續介紹

<com.google.android.material.navigation.NavigationView
 android:id="@+id/navigationView"
  ... />

Add a menu

menu 的部分就是幫我們注入列表式的 navigation item

in layout

<com.google.android.material.navigation.NavigationView
    ...
    app:menu="@menu/navigation_drawer" />

in res/menu/navigation_drawer.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group
        android:id="@+id/group1"
        android:checkableBehavior="single">
        <item
            android:id="@+id/group1_title"
            android:title="Group 1">
            <menu>
                <item
                    android:id="@+id/item1"
                    android:icon="@drawable/ic_baseline_favorite_24"
                    android:title="item 1" />
                <item
                    android:id="@+id/item2"
                    android:icon="@drawable/ic_baseline_link_24"
                    android:title="item 2" />
                <item
                    android:id="@+id/item3"
                    android:icon="@drawable/ic_baseline_search_24"
                    android:title="item 3" />
            </menu>
        </item>
    </group>
    </menu>

Add a header

in layout

<com.google.android.material.navigation.NavigationView
    ...
    app:headerLayout="@layout/header_navigation_drawer" />

in res/layout/header_navigation_drawer.xml

<LinearLayout
    ...
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:layout_marginStart="24dp"
        android:layout_marginEnd="24dp"
        android:textAppearance="?attr/textAppearanceHeadline6"
        android:text="@string/header_title" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="24dp"
        android:layout_marginStart="24dp"
        android:layout_marginEnd="24dp"
        android:textAppearance="?attr/textAppearanceBody2"
        android:textColor="@color/material_on_surface_emphasis_medium"
        android:text="@string/header_text" />

</LinearLayout>

Adding dividers and subtitles

如果想在設計上做出分層分群的話,要從 menu 下手而不是 NavigationView,若對 menu 架構與寫法不熟的話,建議先看過官方文章

In res/menu/navigation_drawer.xml

divider 會自動添加到具有唯一 ID 的 <group> 之間。當子 <menu> 添加到 item 時,它被視為副標題顯示
<group> 當中添加 android:checkableBehavior="single" 可讓選取模式變為單選,一次只能 checked 一個 item

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group
        android:id="@+id/group1"
        android:checkableBehavior="single">
        <item
            android:id="@+id/group1_title"
            android:title="Group 1">
            <menu>
                <item
                    android:id="@+id/item1"
                    android:icon="@drawable/ic_baseline_favorite_24"
                    android:title="item 1" />
                <item
                    android:id="@+id/item2"
                    android:icon="@drawable/ic_baseline_link_24"
                    android:title="item 2" />
                <item
                    android:id="@+id/item3"
                    android:icon="@drawable/ic_baseline_search_24"
                    android:title="item 3" />
            </menu>
        </item>
    </group>
    <group
        android:id="@+id/group2"
        android:checkableBehavior="single">
        <item
            android:id="@+id/group2_title"
            android:title="Group 2">
            <menu>
                <item
                    android:id="@+id/item4"
                    android:icon="@drawable/ic_baseline_favorite_24"
                    android:title="item 4" />
                <item
                    android:id="@+id/item5"
                    android:icon="@drawable/ic_baseline_link_24"
                    android:title="item 5" />
                <item
                    android:id="@+id/item6"
                    android:icon="@drawable/ic_baseline_search_24"
                    android:title="item 6" />
            </menu>
        </item>
    </group>
</menu>

Anatomy

image alt

  1. ContainerHeader (optional)
  2. Divider (optional)
  3. Active text overlay
  4. Active text
  5. Inactive text
  6. Subtitle (optional)
  7. Scrim (optional)

Key properties

Container attributes

Header attributes

Divider attributes

Text overlay attributes

Text attributes

Icon attributes

Subtitle attributes

Scrim attributes


Modal navigation drawer

in layout

Modal drawer 實作上,父層級的佈局要使用 DrawerLayout 才能實現,在此佈局中的作為 drawer view 的子元件,透過設置 android:layout_gravity 來控制 drawer 的出現位置,而這邊要注意若設置 bottom 或 top 會報錯

由於 drawer 都會搭配 toolbar 一起使用,所以在 CoordinatorLayout 的部分都是關於 toolbar 的設置,如果不需要就不用特別使用。若對 toolbar 的設計實作不太懂,可以到我先前寫過的文章在複習一下

<androidx.drawerlayout.widget.DrawerLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true">

            <com.google.android.material.appbar.MaterialToolbar
                android:id="@+id/modal_drawer_toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="@android:color/transparent"
                android:elevation="0dp"
                app:navigationIcon="@drawable/ic_baseline_menu_24"
                app:title="Top bar" />

        </com.google.android.material.appbar.AppBarLayout>

        <!-- Screen content -->
        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">

        </androidx.appcompat.widget.LinearLayoutCompat>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/modal_drawer_navigationView"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        app:menu="@menu/navigation_drawer_menu"
        android:background="@color/darkGrey"
        app:headerLayout="@layout/itemview_drawer_header"
        android:layout_gravity="start" />

</androidx.drawerlayout.widget.DrawerLayout>

windowTranslucentStatus

官方文檔中,實作上建議搭配 status bar 的半透明化讓 drawer 能完整顯示,我的範例中沒特別用到,這邊貼給大家知道一下

<style name="Theme.App" parent="Theme.MaterialComponents.DayNight.*">
    <item name="android:windowTranslucentStatus">true</item>
</style>

in headerLayout

在為 drawer 設置 headerlayout 時,記得要再其父層佈局中加上 android:fitsSystemWindows="true",否則再注入到 drawer 顯示時,Top 的部分不會預留空間,導致 hearder 與 windows 離太近,如圖下

  • In res/layout/header_navigation_drawer.xml
<LinearLayout
    ...
    android:fitsSystemWindows="true">
    ...
</LinearLayout>

in code

編程中,drawerLayout 有 open & close 的方法用來控制 navigationView 的展開與收起

而在 navigationView 能設置 menuItem 點擊事件的 listener,能拿到用戶當前點擊的 menuItem,在點擊後改變它的 checked 狀態,能對應之後寫的 selector 呈現選取前後的狀態改變,讓用戶知道當前是在哪一個 destination,並在之後去關閉 drawer。

這邊我簡單用一個變數去儲存之前點選過的 menuItem,讓在用戶點選新的之後,將其 checked 狀態改變,就能做到 single selected 的效果

private var oldMenuItem :MenuItem?= null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        binding.modalDrawerToolbar.setNavigationOnClickListener {
            binding.drawerLayout.open()
        }
        
        binding.modalDrawerNavigationView.setNavigationItemSelectedListener { menuItem ->
            oldMenuItem?.isChecked = false
            oldMenuItem = menuItem
            menuItem.isChecked = true
            binding.drawerLayout.close()
            true
        }
    }

Bottom navigation drawer

in layout

Bottom 的實作上就與 Modal 有所不同了,更像是 Bottom sheet 的做法。這邊我們總共需要兩層的 CoordinatorLayout,第一層的是用來包裹 Bottom bar 的。第二層就是重點了,包裹著一個 FrameLayout 與 NavigationView,而這邊不知道大家是否有跟我一樣的疑惑,為何要 FrameLayout?

FrameLayout

是作為 Scrim 的,但這系統不是會幫我們實作嗎?上面的 Modal 就是如此,這是因為 Bottom 實作不是透過 DrawerLayout,就如同我一開始講的,實作上類似於 Bottom sheet,而 CoordinatorLayout 是用來協調子 View 之間動作的一個 Layout,並不會實作出 Scrim 的功能,所以我們要用一個 FrameLayout 覆蓋在我們的內容上,作為一個 Scrim

NavigationView

講完 FrameLayout 就回到主角身上,這邊有兩個屬性要特別設置,app:behavior_hideable="true"將 drawer 能被收起隱藏,app:layout_behavior="@string/bottom_sheet_behavior" 將其行為設為 Bottom sheet 的模式

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="?attr/actionBarSize">

        <!-- Screen content -->

        <FrameLayout
            android:id="@+id/scrim"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <com.google.android.material.navigation.NavigationView
            android:id="@+id/bottom_drawer_navigationView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:behavior_hideable="true"
            app:headerLayout="@layout/itemview_drawer_header"
            app:layout_behavior="@string/bottom_sheet_behavior"
            app:menu="@menu/navigation_drawer_menu" />

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottomAppBar"
        style="@style/Widget.MaterialComponents.BottomAppBar.PrimarySurface"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:navigationIcon="@drawable/ic_baseline_menu_24" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

in code

open and close

沒有 drawer 的 open & close 能使用,我們就只能自己實現,藉由改變 BottomSheetBehavior.state,開啟時為 Expanded 收起時為 Hidden

val bottomSheetBehavior = BottomSheetBehavior.from(binding.bottomDrawerNavigationView)
    bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN

binding.bottomAppBar.setNavigationOnClickListener {
    if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED)
            hiddenDrawer(bottomSheetBehavior)
        else
            expandDrawer(bottomSheetBehavior)
    }

private fun hiddenDrawer(bottomSheetBehavior: BottomSheetBehavior<NavigationView>) {
        bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
    }

private fun expandDrawer(bottomSheetBehavior: BottomSheetBehavior<NavigationView>) {
        bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
    }

Navigation Item checked

item checked 的狀態變化設置,就跟我們剛剛在 Modal 實作的相同,用一個變數去儲存之前點選過的 menuItem,讓在用戶點選新的之後,將其 checked 狀態改變,就能做到 single selected 的效果

private var oldMenuItem: MenuItem? = null
binding.bottomDrawerNavigationView.setNavigationItemSelectedListener{ menuItem ->
            oldMenuItem?.isChecked = false
            menuItem.isChecked = true
            oldMenuItem = menuItem
            hiddenDrawer(bottomSheetBehavior)
            true
        }

Setting a scrim

這邊我們就要自己實現 scrim 的效果,功能上就是點擊之後會收起 Bottom drawer

binding.scrim.setOnClickListener {
            hiddenDrawer(bottomSheetBehavior)
    }

而在畫面效果上就比較麻煩了,首先要去設置 Bottom drawer 滑動的 Callback,當中要覆寫兩個功能,這邊我們只需要 onSlider,是因為我們希望隨著用戶滑動收起或展開,scrim 會跟著變化,收起時逐漸變得透明顯示主畫面;展開時逐漸變得明顯遮蓋主畫面。若是對顏色轉化或 ARGB 不太懂的可以看這篇文章

bottomSheetBehavior.addBottomSheetCallback(object :
            BottomSheetBehavior.BottomSheetCallback() {
            override fun onStateChanged(bottomSheet: View, newState: Int) {
            }

            // 實現 scrim 的效果
            override fun onSlide(bottomSheet: View, slideOffset: Float) {
                val baseColor = Color.BLACK
                val baseAlpha = ResourcesCompat.getFloat(
                    resources,
                    com.google.android.material.R.dimen.material_emphasis_medium
                )
                val offset = (slideOffset - (-1f)) / (1f - (-1f)) * (1f - 0f) + 0f
                val alpha = MathUtils.lerp(0f, 255f, offset * baseAlpha).toInt()
                val color = Color.argb(alpha, baseColor.red, baseColor.green, baseColor.blue)
                binding.scrim.setBackgroundColor(color)
            }
        })

Style

Custom Style

自訂風格上,在 Anatomy 可以看到能設置的屬性很多,除了顏色與 text 的字型外,著重在 navigaiton Item 上面,在 check 狀態變換時,呈現不同的色調來告知用戶當前是在哪個 destination

<style name="ThemeOverlay.App.NavigationView" parent="">
        <!-- Container background color-->
        <item name="colorSurface">@color/darkBlue</item>
        <item name="textAppearanceSubtitle2">@style/TextAppearance.App.Subtitle2</item>
        <item name="textAppearanceBody2">@style/TextAppearance.App.Body2</item>
        <item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item>
</style>

<style name="TextAppearance.App.Headline6" parent="TextAppearance.MaterialComponents.Subtitle1">
        <item name="fontFamily">@font/alatsi</item>
        <item name="android:fontFamily">@font/alatsi</item>
</style>

<style name="TextAppearance.App.Subtitle2" parent="TextAppearance.MaterialComponents.Subtitle1">
        <item name="fontFamily">@font/alatsi</item>
        <item name="android:fontFamily">@font/alatsi</item>
</style>

<style name="TextAppearance.App.Body2" parent="TextAppearance.MaterialComponents.Subtitle1">
        <item name="fontFamily">@font/alatsi</item>
        <item name="android:fontFamily">@font/alatsi</item>
</style>

<style name="Widget.App.NavigationView" parent="Widget.MaterialComponents.NavigationView">
        <item name="materialThemeOverlay">@style/ThemeOverlay.App.NavigationView</item>
        <item name="itemIconTint">@color/navigation_item_color</item>
        <item name="itemTextColor">@color/navigation_item_color</item>
        <item name="itemShapeFillColor">@color/navigation_item_background_color</item>
</style>

在 item 的設計上由於要配合 check 狀態變化,所以 color 的部分都寫成 selector 來應用,分別套用在 text 與 icon 的屬性上

In res/color/navigation_item_color.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@android:color/holo_orange_light" android:state_checked="true"/>
    <item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorOnSurface" android:state_enabled="false"/>
    <item android:alpha="@dimen/material_emphasis_medium" android:color="?attr/colorOnSurface"/>
</selector>

In res/color/navigation_item_background_color.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorPrimary" android:state_activated="true"/>
        <item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorPrimary" android:state_checked="true"/>
        <item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorPrimary" android:state_pressed="true"/>
        <item android:color="@android:color/transparent"/>
</selector>

小結

實作上,雖然元件方面 Material Design 幾乎都幫我們包辦好了,但在佈局的設置上,例如 DrawerLayout、Coordinator 上面還是得要我們自行設定,並非直接導入 NavigationView 就能使用

而要注意的是,Modal 與 Bottom 的實現過程並不相同,Bottom 相對來說比較麻煩,要自己實現 scrim 的效果與功能,而 Modal 在使用 drawerLayout 的預設情況下就有了,建議如果只是單純想嘗試看看的,從 Modal 開始會比較好入門

若對實作還是有點不懂的,這邊提供我的 Github 方便大家參考


上一篇
Day 27 - Navigation drawer (Design)
下一篇
Day 29 - Tabs ( Design )
系列文
從 Google Material Design Components 來了解與實作 Android 的 UI/UX 元件設計30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言